package org.sbbs.mojo.exporter;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.components.interactivity.Prompter;
import org.codehaus.plexus.components.interactivity.PrompterException;
import org.hibernate.tool.hbm2x.Exporter;
import org.hibernate.tool.hbm2x.GenericExporter;
import org.hibernate.tool.hbm2x.pojo.EntityPOJOClass;
import org.hibernate.tool.hbm2x.pojo.POJOClass;
import org.sbbs.mojo.SSHExporterMojo;
import org.sbbs.tool.SbbsExporter;
import org.sbbs.tool.ArtifactInstaller;
import org.sbbs.tool.SingleEntityExporter;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Generates Java classes from set of annotated POJOs. Use -DdisableInstallation to prevent installation. If using this
 * goal in a "core" module or project, only DAOs and Managers will be created. For "web" modules, the same principle
 * applies.
 * 
 * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
 * @goal gen
 * @phase generate-sources
 * @execute phase="compile"
 */
public class SSHGeneratorMojo
    extends SSHExporterMojo {
    String functionType;

    String masterPojoName = null, detailPojoName = null;

    boolean generateCoreOnly;

    boolean generateWebOnly;

    String pojoName;

    /**
     * This is a prompter that can be user within the maven framework.
     * 
     * @component
     */
    Prompter prompter;

    /**
     * The path where the generated artifacts will be placed. This is intentionally not set to the default location for
     * maven generated sources. This is to keep these files out of the eclipse/idea generated sources directory as the
     * intention is that these files will be copied to a source directory to be edited and modified and not re generated
     * each time the plugin is run. If you want to regenerate the files each time you build the project just set this
     * value to ${basedir}/target/generated-sources or set the flag on eclipse/idea plugin to include this file in your
     * project file as a source directory.
     * 
     * @parameter expression="${appfuse.destinationDirectory}" default-value="${basedir}"
     * @noinspection UnusedDeclaration
     */
    private String destinationDirectory;

    /**
     * The directory containing the source code.
     * 
     * @parameter expression="${appfuse.sourceDirectory}" default-value="${basedir}/target/ssh/generated-sources"
     * @noinspection UnusedDeclaration
     */
    private String sourceDirectory;

    /**
     * Allows disabling installation - for tests and end users that don't want to do a full installation
     * 
     * @parameter expression="${appfuse.disableInstallation}" default-value="false"
     */
    private boolean disableInstallation;

    /**
     * @parameter expression="${appfuse.genericCore}" default-value="true"
     * @noinspection UnusedDeclaration
     */
    private boolean genericCore;

    /**
     * @parameter expression="${appfuse.templateDirectory}" default-value="${basedir}/src/test/resources"
     * @noinspection UnusedDeclaration
     */
    private String templateDirectory;

    /**
     * Path for where to generate files at.
     */

    private String fullPath = null;

    /**
     * Default constructor.
     */
    public SSHGeneratorMojo() {
        addDefaultComponent( "target/ssh/generated-sources", "configuration", false );
        addDefaultComponent( "target/ssh/generated-sources", "annotationconfiguration", true );
    }

    // --------------------- Interface ExporterMojo ---------------------

    /**
     * Returns <b>gen</b>.
     * 
     * @return String goal's name
     */
    public String getName() {
        return "gen";
    }

    @Override
    public void execute()
        throws MojoExecutionException, MojoFailureException {
        // if project is of type "pom", throw an error
        if ( getProject().getPackaging().equalsIgnoreCase( "pom" ) ) {
            String errorMsg = "[ERROR] This plugin cannot be run from a pom project, please run it from a jar or war project (i.e. core or web).";
            // getLog().error(errorMsg);
            throw new MojoFailureException( errorMsg );
        }
        /*
         * TODO 此处加入命令行参数输入
         */
        if ( functionType == null )
            functionType = System.getProperty( "ftype" );
        if ( functionType == null ) {
            try {
                functionType = prompter.prompt( "please select your function type ,single entity:1 master detail:2?", "1" );
            }
            catch ( PrompterException e ) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        if ( functionType.equalsIgnoreCase( "1" ) ) {
            if ( functionType == null || "".equals( functionType.trim() ) ) {
                throw new MojoExecutionException( "You must specify an entity name to continue." );
            }
            if ( pojoName == null )
                pojoName = System.getProperty( "entity" );

            if ( pojoName == null ) {
                try {
                    pojoName = prompter.prompt( "What is the name of your pojo (i.e. Person)?" );
                }
                catch ( PrompterException pe ) {
                    pe.printStackTrace();
                }
            }
            genExec();
        }
        else {

            try {
                masterPojoName =
                    prompter.prompt( "please input master pojo Name with full package path,and the entity java file in model package,like org.sbbs.app.model.DemoEntity:" );
                detailPojoName =
                    prompter.prompt( "please input detail pojo Name with full package path,and the entity java file in model package,like org.sbbs.app.model.DemoEntity:" );

            }
            catch ( PrompterException e ) {
                e.printStackTrace();
            }
            pojoName = masterPojoName;
            this.processPojoName();
            addEntityToHibernateCfgXml();
            pojoName = detailPojoName;
            this.processPojoName();
            addEntityToHibernateCfgXml();

            pojoName = masterPojoName;
            this.processPojoName();
            super.execute();
            pojoName = detailPojoName;
            this.processPojoName();
            super.execute();

        }

    }

    private void genExec()
        throws MojoExecutionException, MojoFailureException {
        this.processPojoName();
        addEntityToHibernateCfgXml();
        super.execute();
    }

    private void processPojoName()
        throws MojoExecutionException, MojoFailureException {
        if ( pojoName == null || "".equals( pojoName.trim() ) ) {
            throw new MojoExecutionException( "You must specify an entity name to continue." );
        }

        // A dot in the entity name means the person is specifying the
        // package.
        if ( pojoName.contains( "." ) ) {
            if ( pojoName.indexOf( "model" ) == -1 ) {
                throw new MojoExecutionException( "You must specify 'model' as the last package in your entity name." );
            }
            System.setProperty( "entity", pojoName );
            fullPath = pojoName.substring( 0, pojoName.indexOf( ".model" ) );
            // allow ~ to be used for groupId
            fullPath = fullPath.replace( "~", getProject().getGroupId() );

            pojoName = pojoName.substring( pojoName.lastIndexOf( "." ) + 1 );
            log( "Package name set to: " + fullPath );
        }
    }

    /**
     * TODO 好像额外的信息需要在这里进行添加配置，以供具体的导出使用
     * 
     * @see org.sbbs.mojo.SSHExporterMojo#configureExporter(org.hibernate.tool.hbm2x.Exporter)
     */
    protected Exporter configureExporter( Exporter exp )
        throws MojoExecutionException {
        // Read in AppFuseExporter#configureExporter to decide if a class should
        // be generated or not
        System.setProperty( "appfuse.entity", pojoName );

        // add output directory to compile roots
        getProject().addCompileSourceRoot( new File( getComponent().getOutputDirectory() ).getPath() );

        // now set the extra properties for the AppFuseExporter
        SbbsExporter exporter = (SbbsExporter) super.configureExporter( exp );
        exporter.getProperties().setProperty( "ejb3", getComponentProperty( "ejb3", "true" ) );
        exporter.getProperties().setProperty( "jdk5", getComponentProperty( "jdk5", "true" ) );

        if ( generateCoreOnly ) {
            exporter.getProperties().setProperty( "generate-core", "true" );
        }
        else if ( generateWebOnly ) {
            exporter.getProperties().setProperty( "generate-web", "true" );
        }

        String rootPackage = ( fullPath != null ) ? fullPath : getProject().getGroupId();

        // AppFuse-specific values

        exporter.getProperties().setProperty( "fType", this.functionType );
        // TODO 主子关系时需要额外添加的属性
        if ( this.functionType.equals( "2" ) ) {
            // 子类全路径名
            exporter.getProperties().setProperty( "detailFullPathName", this.detailPojoName );
            POJOClass detailPOJOClass = getPojoClassByFullPojoName( this.detailPojoName, exporter );

            // 子类shortName
            String detailShortName = detailPOJOClass.getShortName();
            exporter.getProperties().setProperty( "detailShortName", detailShortName );

            // 子类shortNameLower
            String detailShortNameLower = detailShortName.substring( 0, 1 ).toLowerCase() + detailShortName.substring( 1 );
            exporter.getProperties().setProperty( "detailShortNameLower", detailShortNameLower );

            POJOClass masterPOJOClass = getPojoClassByFullPojoName( this.masterPojoName, exporter );

            // 子类shortName
            String masterShortName = masterPOJOClass.getShortName();
            exporter.getProperties().setProperty( "masterShortName", masterShortName );
            // 主类shortNameLower
            String masterShortNameLower =
                masterPOJOClass.getShortName().substring( 0, 1 ).toLowerCase() + masterPOJOClass.getShortName().substring( 1 );
            exporter.getProperties().setProperty( "masterShortNameLower", masterShortNameLower );
            // 主类id类型
            String masterIdType = masterPOJOClass.getJavaTypeName( masterPOJOClass.getIdentifierProperty(), true );
            exporter.getProperties().setProperty( "masterIdType", masterIdType );
            // 主类id名
            String masterIdName = masterPOJOClass.getIdentifierProperty().getName();
            exporter.getProperties().setProperty( "masterIdName", masterIdName );
        }

        exporter.getProperties().setProperty( "basepackage", rootPackage );
        exporter.getProperties().setProperty( "daoframework", getProject().getProperties().getProperty( "dao.framework" ) );

        String webFramework =
            ( getProject().getProperties().containsKey( "web.framework" ) ) ? getProject().getProperties().getProperty( "web.framework" ) : "";

        exporter.getProperties().setProperty( "webframework", webFramework );

        exporter.getProperties().setProperty( "packaging", getProject().getPackaging() );
        exporter.getProperties().setProperty( "genericcore", String.valueOf( genericCore ) );

        if ( templateDirectory != null ) {
            exporter.getProperties().setProperty( "templatedirectory", templateDirectory );
        }

        exporter.getProperties().setProperty( "appfusepackage", rootPackage );

        // See if the project has security enabled
        /*
         * boolean hasSecurity = false; if ( getProject().getPackaging().equals( "war" ) ) { Collection<File>
         * sourceFiles = FileUtils.listFiles( getProject().getBasedir(), new String[] { "xml" }, true ); for ( File file
         * : sourceFiles ) { if ( file.getPath().contains( "security.xml" ) ) { hasSecurity = true; break; } } }
         */

        // exporter.getProperties().setProperty( "hasSecurity", String.valueOf( hasSecurity ) );

        // determine if using Home or MainMenu for Tapestry
        /*
         * if ( webFramework.equals( "tapestry" ) ) { boolean useMainMenu = true; Collection<File> sourceFiles =
         * FileUtils.listFiles( getProject().getBasedir(), new String[] { "java" }, true ); for ( File file :
         * sourceFiles ) { if ( file.getPath().contains( "Home.java" ) ) { useMainMenu = false; break; } }
         * exporter.getProperties().setProperty( "useMainMenu", String.valueOf( useMainMenu ) ); }
         */

        return exporter;
    }

    private POJOClass getPojoClassByFullPojoName( String fpn, GenericExporter ge ) {
        EntityPOJOClass e = null;
        Iterator iterator = ge.getCfg2JavaTool().getPOJOIterator( ge.getConfiguration().getClassMappings() );
        Map additionalContext = new HashMap();
        while ( iterator.hasNext() ) {
            e = (EntityPOJOClass) iterator.next();
            String n = e.getClass().getName();
            if ( ( e.getPackageName() + "." + e.getShortName() ).equals( fpn ) )
                return e;
            else
                e = null;
        }
        return e;
    }

    /**
     * Executes the plugin in an isolated classloader.
     * 
     * @throws MojoExecutionException When there is an erro executing the plugin
     */
    @Override
    protected void doExecute()
        throws MojoExecutionException {
        super.doExecute();

        if ( System.getProperty( "disableInstallation" ) != null ) {
            disableInstallation = Boolean.valueOf( System.getProperty( "disableInstallation" ) );
        }

        // allow installation to be suppressed when testing
        if ( !disableInstallation ) {
            ArtifactInstaller installer =
                new ArtifactInstaller( getProject(), pojoName, sourceDirectory, destinationDirectory, this.fullPath, genericCore );
            installer.execute();
        }
    }

    /**
     * Instantiates a org.appfuse.tool.AppFuseExporter object. 根据功能类型返回相应的exporter
     * 
     * @return POJOExporter
     */
    protected Exporter createExporter() {

        if ( this.functionType.equalsIgnoreCase( "1" ) )
            return new SingleEntityExporter();
        else
            // return new AppFuseExporter();
            return null;
    }

    protected void setGenerateCoreOnly( boolean generateCoreOnly ) {
        this.generateCoreOnly = generateCoreOnly;
    }

    protected void setGenerateWebOnly( boolean generateWebOnly ) {
        this.generateWebOnly = generateWebOnly;
    }

    private void log( String msg ) {
        getLog().info( "[AppFuse] " + msg );
    }

    private void addEntityToHibernateCfgXml()
        throws MojoFailureException {

        String daoFramework = getProject().getProperties().getProperty( "dao.framework" );

        // If dao.framework is jpa, change to jpaconfiguration and
        // persistence.xml should be found in classpath.
        // No other configuration is needed.
        /*
         * if (daoFramework.indexOf("jpa") > -1) { getComponentProperties().put("implementation", "jpaconfiguration");
         * checkEntityExists(); }
         */

        // for war projects that have a parent pom, don't reset classpath
        // this is to allow using hibernate.cfg.xml from core module
        /*
         * if (getProject().getPackaging().equals("war") && getProject().hasParent()) { // assume first module in parent
         * project has hibernate.cfg.xml String moduleName = (String) getProject().getParent().getModules() .get(0);
         * String pathToParent = getProject().getOriginalModel().getParent() .getRelativePath(); pathToParent =
         * pathToParent.substring(0, pathToParent.lastIndexOf('/') + 1); log("Assuming '" + moduleName +
         * "' has hibernate.cfg.xml in its src/main/resources directory"); getComponentProperties().put(
         * "configurationfile", getProject().getBasedir() + "/" + pathToParent + moduleName +
         * "/src/main/resources/hibernate.cfg.xml"); }
         */

        if ( getComponentProperty( "configurationfile" ) == null ) {
            if ( getComponentProperty( "isTest" ).equals( "true" ) ) {
                getComponentProperties().put( "configurationfile", "src/test/resources/hibernate.cfg.xml" );
            }
            else
                getComponentProperties().put( "configurationfile", "src/main/resources/hibernate.cfg.xml" );
        }

        // if entity is not in hibernate.cfg.xml, add it
        String hibernateConfig = getComponentProperty( "configurationfile" );
        String hibernateCfgXml = null;// FileUtils.readFileToString(new File( hibernateConfig));

        if ( daoFramework.equals( "hibernate" ) ) {
            try {
                hibernateCfgXml = FileUtils.readFileToString( new File( hibernateConfig ) );
                // addEntityToHibernateCfgXml(hibernateCfgXml);
            }
            catch ( IOException io ) {
                throw new MojoFailureException( io.getMessage() );
            }
        }

        String className = ( fullPath != null ) ? fullPath : getProject().getGroupId();

        className += ".model." + pojoName;
        log( "Constructed class name : " + className );

        POJOSearcher pojoSearcher = new POJOSearcher( hibernateCfgXml );
        if ( !pojoSearcher.searchForPojo( pojoName ) ) {
            // check that class exists and has an @Entity annotation
            checkEntityExists();

            hibernateCfgXml =
                hibernateCfgXml.replace( "</session-factory>", "    <mapping class=\"" + className + "\"/>" + "\n    </session-factory>" );
            log( "Adding '" + pojoName + "' to hibernate.cfg.xml..." );
        }

        hibernateCfgXml = hibernateCfgXml.replaceAll( "\\$\\{appfusepackage}", getProject().getGroupId() );

        try {
            FileUtils.writeStringToFile( new File( getComponentProperty( "configurationfile", "src/main/resources/hibernate.cfg.xml" ) ),
                                         hibernateCfgXml );
        }
        catch ( IOException io ) {
            throw new MojoFailureException( io.getMessage() );
        }
    }

    private void checkEntityExists()
        throws MojoFailureException {
        // allow check to be bypassed when -Dentity.check=false
        if ( !"false".equals( System.getProperty( "entity.check" ) ) ) {
            final String FILE_SEP = System.getProperty( "file.separator" );
            // remove isTest from final and add null check.20121123 by Mr.fdc
            String isTest = System.getProperty( "isTest" );
            if ( isTest == null )
                isTest = "false";

            String modelTypePath = "main";
            if ( isTest.equalsIgnoreCase( "true" ) )
                modelTypePath = "test";
            String pathToModelPackage = "src" + FILE_SEP + modelTypePath + FILE_SEP + "java" + FILE_SEP;
            if ( getProject().getPackaging().equals( "war" ) && getProject().hasParent() ) {
                String moduleName = (String) getProject().getParent().getModules().get( 0 );
                String pathToParent = getProject().getOriginalModel().getParent().getRelativePath();
                pathToParent = pathToParent.substring( 0, pathToParent.lastIndexOf( '/' ) + 1 );
                pathToParent = pathToParent.replaceAll( "/", FILE_SEP );
                pathToModelPackage = getProject().getBasedir() + FILE_SEP + pathToParent + moduleName + "/" + pathToModelPackage;
            }

            // refactor to check classpath instead of filesystem
            String groupIdAsPath = getProject().getGroupId().replace( ".", FILE_SEP );

            // String fullPathPackage = pathToModelPackage + groupIdAsPath;
            String fullPathPackage = pathToModelPackage;// remove groupIdAsPath
                                                        // var because entity
                                                        // input full path name
            if ( fullPath != null ) {
                fullPathPackage += FILE_SEP + fullPath.replace( ".", FILE_SEP ) + FILE_SEP + "model";// add model path
            }
            else {
                fullPathPackage += FILE_SEP + "model";
            }
            log( "Looking for entity in " + fullPathPackage );

            File modelPackage = new File( this.destinationDirectory + "\\" + fullPathPackage );// add
                                                                                               // this.destinationDirectory
                                                                                               // to the
                                                                                               // file path
            boolean entityExists = false;

            if ( modelPackage.exists() ) {
                String[] entities = modelPackage.list();
                for ( String entity : entities ) {
                    log( "Found '" + entity + "' in model package..." );
                    if ( entity.contains( pojoName + ".java" ) ) {
                        entityExists = true;
                        break;
                    }
                }
            }
            else {
                getLog().error( "The path '" + fullPathPackage + groupIdAsPath + FILE_SEP + "model' does not exist!" );
            }

            if ( !entityExists ) {
                throw new MojoFailureException( "[ERROR] The '" + pojoName + "' entity does not exist in '" + modelPackage + "'." );
            }
            else {
                // Entity found, make sure it has @Entity annotation
                try {
                    File pojoFile = new File( modelPackage + FILE_SEP + pojoName + ".java" );
                    String entityAsString = FileUtils.readFileToString( pojoFile );
                    if ( !entityAsString.contains( "@Entity" ) ) {
                        String msg = "Entity '" + pojoName + "' found, but it doesn't contain an @Entity annotation. Please add one.";
                        throw new MojoFailureException( msg );
                    }
                }
                catch ( IOException io ) {
                    throw new MojoFailureException( "[ERROR] Class '" + pojoName + ".java' not found in '" + modelPackage + "'" );
                }
            }
        }
    }

    public class POJOSearcher
        extends DefaultHandler {
        private String pojoName;

        private boolean foundPojo;

        private String xmlString;

        public POJOSearcher( String xmlString ) {
            this.xmlString = xmlString;
        }

        public boolean searchForPojo( String pojoName ) {
            this.pojoName = pojoName;
            this.foundPojo = false;

            SAXParserFactory spf = SAXParserFactory.newInstance();
            try {
                SAXParser sp = spf.newSAXParser();
                // sp.setProperty("http://apache.org/xml/features/nonvalidating/load-external-dtd",
                // false);

                sp.parse( new InputSource( new StringReader( xmlString ) ), this );
            }
            catch ( Exception ex ) {
                System.out.println( ex.getMessage() );
                ex.printStackTrace();
            }

            return foundPojo;
        }

        @Override
        public void startElement( String uri, String localName, String name, Attributes attributes )
            throws SAXException {
            super.startElement( uri, localName, name, attributes );
            if ( name.equals( "mapping" ) ) {
                String classValue = attributes.getValue( "class" );
                if ( classValue != null ) {
                    if ( classValue.endsWith( pojoName ) ) {
                        foundPojo = true;
                    }
                }
            }
        }
    }
}
